-
Notifications
You must be signed in to change notification settings - Fork 408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reduce memory allocations by writing directly into the output stream (#461) #472
Conversation
Regarding commit 3d68f91, I'm not sure it is actually a good idea to make use of Jackson internal utilities like Note also that New buffers are allocated dynamically as data is written into the ByteBufferBuilder. They are allocated on the heap directly and don't come from the BufferRecycler (see ByteArrayBuilder.java#L294). As the comments says in the code, the size of the newly allocated buffer is half the total size of what is allocated so far (with a max value). Let's take an example and create a new ByteBufferBuilder and give it a BufferRecycler. A first byte array of 2000 bytes is allocated (cold start). Then we write 3001 bytes - here is what happens:
When done, you need to call Let's run the example a second time with the same recycler (purpose is to somehow reuse previous byte arrays to reduce memory allocation). We create a new ByteArrayBuilder that immediately asks the recycler for a first initial buffer. Unfortunately, the recycler now holds an array of size 1500 which is LESS than the hardcoded minimum set to 2000... So instead of returning the cached array, it discards it and creates a new one of 2000 bytes. The rest happens exactly as during the first - cold - run. Conclusion: we have not saved/reused anything at all! I'm puzzled to be honest. I don't see how this mechanism can provide an efficient caching... except when what you write never (or rarely) exceeds the hardcoded minimum (2000 bytes). My feeling is that we'd better write our own mechanism instead of relying of something that is especially designed for use by Jackson internals only... May be I should create an issue on Jackson-Core and see what Tatu has to say about it... (no doubt it will first tell me I should not use it). |
Additional notes: (1) For information, in production we ingest approximatively 15K events/s - for one hour during a working day, average JSON size is around I quite like this solution. I wonder if we need to go any further... After all, the rest of the chain is not optimised... look at the Layout for instance... it returns a String... and most Logback components are not designed to Stream their output... (2) |
…f returning a byte array Introduce a new (internal) StreamingEncoder interface to be implemented by Encoders that supports writing directly into the output stream instead of returning their results as a byte array. Update both the AbstractLogstashTcpSocketAppender and the CompositeJsonEncoder to support this new interface. This should hoppefully reduce the amount of short-lived byte arrays created for each log event. See logfellow#461 for more information.
… ByteArrayOutputStream for every log event
In general, the StreamingEncoder implementation looks great! I'm happy to incorporate it into logstash-logback-encoder.
The fact that it only returned the tail buffer was surprising to me. Thanks for the detailed analysis! Good stuff. A custom buffer recycler sounds perfectly reasonable, unless it is crazy complex. Want to submit it as an alternate PR for comparison? |
Hi Phil... I'm back with this optimisation. A few words about what I did so far... The
The The I made a small benchmark using JMH to compare this implementation with version 6.6. Here are the results:
Not bad... especially with regards to the positive impacts on memory and GC.
This of course requires additional changes. I propose to cover it in a separate issue. The improvements provided by this PR already seem to be good enough ;-) Tell me what you think... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good!
The performance numbers you posted are impressive.
My comments are mostly minor. Overall it's a really great implementation.
src/main/java/net/logstash/logback/util/ThreadLocalBuffers.java
Outdated
Show resolved
Hide resolved
src/main/java/net/logstash/logback/composite/CompositeJsonFormatter.java
Outdated
Show resolved
Hide resolved
src/main/java/net/logstash/logback/encoder/CompositeJsonEncoder.java
Outdated
Show resolved
Hide resolved
src/main/java/net/logstash/logback/util/ReusableByteBuffer.java
Outdated
Show resolved
Hide resolved
src/main/java/net/logstash/logback/util/ReusableByteBuffer.java
Outdated
Show resolved
Hide resolved
src/main/java/net/logstash/logback/util/ReusableByteBuffers.java
Outdated
Show resolved
Hide resolved
src/main/java/net/logstash/logback/util/ReusableByteBuffers.java
Outdated
Show resolved
Hide resolved
src/main/java/net/logstash/logback/util/ReusableByteBuffer.java
Outdated
Show resolved
Hide resolved
src/main/java/net/logstash/logback/util/ReusableByteBuffers.java
Outdated
Show resolved
Hide resolved
Thanks again for your work on this! Very cool improvement. (And thanks for addressing all my code review comments. I know I can be picky at times. ;) Cheers! |
👍 |
I think the import order was all over the place when I originally enabled the checkstyle rules. I tried to create rules that matched the existing codebase as much as possible to minimize reformatting, and to "stop the bleeding". Having said that, I'm not against defining import order rules and reorganizing the existing imports. If there is a clear majority practice, I'd like to stick with it. But I'm less concerned about changing imports than reformatting code in general because I rarely use git blame to look at imports. I'm usually looking at actual code when using git blame, and therefore am much more hesitant to reformat the actual code. |
Reduce GC pressure by writing directly into the output stream instead of returning a byte array.
Introduce a new (internal) StreamingEncoder interface to be implemented by Encoders that supports writing directly into the output stream instead of returning their results as a byte array.
Update both the AbstractLogstashTcpSocketAppender and the CompositeJsonEncoder to support this new interface. This should hoppefully reduce the amount of short-lived byte arrays created for each log event.
Review test case and make use of actual instances instead of mocks (required to preserve state during test otherwise conditional operations like start/isStarted do not work as expected).
See #461 for more information.